/*
 * Copyright 2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.seppiko.chart.utils

import com.google.zxing.BarcodeFormat
import com.google.zxing.Dimension
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import com.google.zxing.datamatrix.encoder.*
import com.google.zxing.pdf417.PDF417Writer
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.google.zxing.qrcode.encoder.ByteMatrix
import com.google.zxing.qrcode.encoder.Encoder
import org.seppiko.chart.exceptions.SeppikoCheckException
import org.seppiko.chart.models.BarcodeEntity
import org.seppiko.commons.utils.CharUtil
import java.lang.StringBuilder
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.*

/**
 *
 * @author Leonard Woo
 */
object ZxingUtil {
  private val logger: LoggingManager = LoggingManager.INSTANCE.getLogger(ZxingUtil::class.qualifiedName!!)

  private val CRLF: String = "" + CharUtil.CARRIAGE_RETURN + CharUtil.LINE_FEED

  fun convertByteArrays(bas: Array<ByteArray>, nl: String? = "" + CRLF): String {
    val sb = StringBuilder()
    for (i in bas.indices) {
      for (j in bas[0].indices) {
        sb.append(bas[i][j].toInt())
      }
      sb.append(nl)
    }
    return sb.toString()
  }

  fun convertBooleanArray(data: BooleanArray): String {
    val sb = StringBuilder()
    for (i in data.indices) {
      sb.append(if (data[i]) "1" else "0")
    }
    return sb.toString()
  }

  val qrCode: QRcode = QRcode()
  val pdf417: Pdf417 = Pdf417()
  val dataMatrix: Datamatrix = Datamatrix()

  class QRcode {
    @Throws(WriterException::class, SeppikoCheckException::class)
    fun encode(p: BarcodeEntity): ByteMatrix {
      val hints = EnumMap<EncodeHintType, Any?>(
        EncodeHintType::class.java
      )
      if (StandardCharsets.ISO_8859_1 != Charset.forName(p.encoding)) {
        // Only set if not QR code default
        hints[EncodeHintType.CHARACTER_SET] = p.encoding
      }
      return Encoder.encode(p.data, parseErrorCorrectLevel(p.errorCorrectionLevel), hints).matrix
    }

    companion object {
      @Throws(SeppikoCheckException::class)
      fun parseErrorCorrectLevel(ecl: String): ErrorCorrectionLevel {
        return if ("L".equals(ecl, ignoreCase = true)) {
          ErrorCorrectionLevel.L
        } else if ("Q".equals(ecl, ignoreCase = true)) {
          ErrorCorrectionLevel.Q
        } else if ("M".equals(ecl, ignoreCase = true)) {
          ErrorCorrectionLevel.M
        } else if ("H".equals(ecl, ignoreCase = true)) {
          ErrorCorrectionLevel.H
        } else {
          logger.info("Invalid error correct level : $ecl")
          throw SeppikoCheckException("Invalid error correct level : $ecl")
        }
      }
    }
  }

  class Pdf417 {
    @Throws(WriterException::class)
    fun encode(p: BarcodeEntity): BitMatrix {
      val hints = EnumMap<EncodeHintType, Any?>(
        EncodeHintType::class.java
      )
      hints[EncodeHintType.CHARACTER_SET] = p.encoding
      hints[EncodeHintType.ERROR_CORRECTION] = p.errorCorrectionLevel
      hints[EncodeHintType.MARGIN] = p.margin
      return PDF417Writer().encode(p.data, BarcodeFormat.PDF_417, p.width, p.height, hints)
    }
  }

  class Datamatrix {
    fun encode(contents: String?): ByteMatrix {
      val shape = SymbolShapeHint.FORCE_SQUARE
      val minSize: Dimension? = null
      val maxSize: Dimension? = null

      val encoded = HighLevelEncoder.encodeHighLevel(contents, shape, minSize, maxSize)
      val symbolInfo = SymbolInfo.lookup(encoded.length, shape, minSize, maxSize, true)

      val placement = DefaultPlacement(
        ErrorCorrection.encodeECC200(encoded, symbolInfo),
        symbolInfo.symbolDataWidth,
        symbolInfo.symbolDataHeight)

      placement.place()

      return encodeLowLevel(placement, symbolInfo)
    }

    private fun encodeLowLevel(placement: DefaultPlacement, symbolInfo: SymbolInfo): ByteMatrix {
      val symbolWidth = symbolInfo.symbolDataWidth
      val symbolHeight = symbolInfo.symbolDataHeight
      val matrix = ByteMatrix(symbolInfo.symbolWidth, symbolInfo.symbolHeight)
      var matrixY = 0
      for (y in 0 until symbolHeight) {
        var matrixX: Int
        if (y % symbolInfo.matrixHeight == 0) {
          matrixX = 0
          for (x in 0 until symbolInfo.symbolWidth) {
            matrix[matrixX, matrixY] = x % 2 == 0
            matrixX++
          }
          matrixY++
        }
        matrixX = 0
        for (x in 0 until symbolWidth) {
          // Fill the right edge with full 1
          if (x % symbolInfo.matrixWidth == 0) {
            matrix[matrixX, matrixY] = true
            matrixX++
          }
          matrix[matrixX, matrixY] = placement.getBit(x, y)
          matrixX++
          // Fill the right edge with alternate 0 / 1
          if (x % symbolInfo.matrixWidth == symbolInfo.matrixWidth - 1) {
            matrix[matrixX, matrixY] = y % 2 == 0
            matrixX++
          }
        }
        matrixY++
        // Fill the bottom edge with full 1
        if (y % symbolInfo.matrixHeight == symbolInfo.matrixHeight - 1) {
          matrixX = 0
          for (x in 0 until symbolInfo.symbolWidth) {
            matrix[matrixX, matrixY] = true
            matrixX++
          }
          matrixY++
        }
      }
      return matrix
    }
  }

}